/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { Text } from 'ink';
import { useEffect, useState } from 'react';
import { FixedDeque } from 'mnemonist';
import { theme } from '../semantic-colors.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { debugState } from '../debug.js';
import { appEvents, AppEvent } from '../../utils/events.js';
import { debugLogger } from '@google/gemini-cli-core';

// Frames that render at least this far before or after an action are considered
// idle frames.
const MIN_TIME_FROM_ACTION_TO_BE_IDLE = 500;

export const ACTION_TIMESTAMP_CAPACITY = 2048;
export const FRAME_TIMESTAMP_CAPACITY = 2048;

// Exported for testing purposes.
export const profiler = {
  profilersActive: 0,
  numFrames: 0,
  totalIdleFrames: 0,
  totalFlickerFrames: 0,
  hasLoggedFirstFlicker: false,
  lastFrameStartTime: 0,
  openedDebugConsole: false,
  lastActionTimestamp: 0,

  possiblyIdleFrameTimestamps: new FixedDeque<number>(
    Array,
    FRAME_TIMESTAMP_CAPACITY,
  ),
  actionTimestamps: new FixedDeque<number>(Array, ACTION_TIMESTAMP_CAPACITY),

  reportAction() {
    const now = Date.now();
    if (now - this.lastActionTimestamp > 16) {
      if (this.actionTimestamps.size >= ACTION_TIMESTAMP_CAPACITY) {
        this.actionTimestamps.shift();
      }
      this.actionTimestamps.push(now);
      this.lastActionTimestamp = now;
    }
  },

  reportFrameRendered() {
    if (this.profilersActive === 0) {
      return;
    }
    const now = Date.now();
    this.lastFrameStartTime = now;
    this.numFrames++;
    if (debugState.debugNumAnimatedComponents === 0) {
      if (this.possiblyIdleFrameTimestamps.size >= FRAME_TIMESTAMP_CAPACITY) {
        this.possiblyIdleFrameTimestamps.shift();
      }
      this.possiblyIdleFrameTimestamps.push(now);
    } else {
      // If a spinner is present, consider this an action that both prevents
      // this frame from being idle and also should prevent a follow on frame
      // from being considered idle.
      if (this.actionTimestamps.size >= ACTION_TIMESTAMP_CAPACITY) {
        this.actionTimestamps.shift();
      }
      this.actionTimestamps.push(now);
    }
  },

  checkForIdleFrames() {
    const now = Date.now();
    const judgementCutoff = now - MIN_TIME_FROM_ACTION_TO_BE_IDLE;
    const oneSecondIntervalFromJudgementCutoff = judgementCutoff - 1000;

    let idleInPastSecond = 0;

    while (
      this.possiblyIdleFrameTimestamps.size > 0 &&
      this.possiblyIdleFrameTimestamps.peekFirst()! <= judgementCutoff
    ) {
      const frameTime = this.possiblyIdleFrameTimestamps.shift()!;
      const start = frameTime - MIN_TIME_FROM_ACTION_TO_BE_IDLE;
      const end = frameTime + MIN_TIME_FROM_ACTION_TO_BE_IDLE;

      while (
        this.actionTimestamps.size > 0 &&
        this.actionTimestamps.peekFirst()! < start
      ) {
        this.actionTimestamps.shift();
      }

      const hasAction =
        this.actionTimestamps.size > 0 &&
        this.actionTimestamps.peekFirst()! <= end;

      if (!hasAction) {
        if (frameTime >= oneSecondIntervalFromJudgementCutoff) {
          idleInPastSecond++;
        }
        this.totalIdleFrames++;
      }
    }

    if (idleInPastSecond >= 5) {
      if (this.openedDebugConsole === false) {
        this.openedDebugConsole = true;
        appEvents.emit(AppEvent.OpenDebugConsole);
      }
      debugLogger.error(
        `${idleInPastSecond} frames rendered while the app was ` +
          `idle in the past second. This likely indicates severe infinite loop ` +
          `React state management bugs.`,
      );
    }
  },

  registerFlickerHandler(constrainHeight: boolean) {
    const flickerHandler = () => {
      // If we are not constraining the height, we are intentionally
      // overflowing the screen.
      if (!constrainHeight) {
        return;
      }

      this.totalFlickerFrames++;
      this.reportAction();

      if (!this.hasLoggedFirstFlicker) {
        this.hasLoggedFirstFlicker = true;
        debugLogger.error(
          'A flicker frame was detected. This will cause UI instability. Type `/profile` for more info.',
        );
      }
    };
    appEvents.on(AppEvent.Flicker, flickerHandler);
    return () => {
      appEvents.off(AppEvent.Flicker, flickerHandler);
    };
  },
};

export const DebugProfiler = () => {
  const { showDebugProfiler, constrainHeight } = useUIState();
  const [forceRefresh, setForceRefresh] = useState(0);

  // Effect for listening to stdin for keypresses and stdout for resize events.
  useEffect(() => {
    profiler.profilersActive++;
    const stdin = process.stdin;
    const stdout = process.stdout;

    const handler = () => {
      profiler.reportAction();
    };

    stdin.on('data', handler);
    stdout.on('resize', handler);

    return () => {
      stdin.off('data', handler);
      stdout.off('resize', handler);
      profiler.profilersActive--;
    };
  }, []);

  useEffect(() => {
    const updateInterval = setInterval(() => {
      profiler.checkForIdleFrames();
    }, 1000);
    return () => clearInterval(updateInterval);
  }, []);

  useEffect(
    () => profiler.registerFlickerHandler(constrainHeight),
    [constrainHeight],
  );

  // Effect for updating stats
  useEffect(() => {
    if (!showDebugProfiler) {
      return;
    }
    // Only update the UX infrequently as updating the UX itself will cause
    // frames to run so can disturb what we are measuring.
    const forceRefreshInterval = setInterval(() => {
      setForceRefresh((f) => f + 1);
      profiler.reportAction();
    }, 4000);
    return () => clearInterval(forceRefreshInterval);
  }, [showDebugProfiler]);

  if (!showDebugProfiler) {
    return null;
  }

  return (
    <Text color={theme.status.warning} key={forceRefresh}>
      Renders: {profiler.numFrames} (total),{' '}
      <Text color={theme.status.error}>{profiler.totalIdleFrames} (idle)</Text>,{' '}
      <Text color={theme.status.error}>
        {profiler.totalFlickerFrames} (flicker)
      </Text>
    </Text>
  );
};
